Skip to content

test(reservations): M2 test-coverage backfill (7 of 8 items) - #4280

Merged
piotr-roslaniec merged 18 commits into
reservations-epicfrom
m1/reservation-test-coverage-backfill
Sep 3, 2026
Merged

test(reservations): M2 test-coverage backfill (7 of 8 items)#4280
piotr-roslaniec merged 18 commits into
reservations-epicfrom
m1/reservation-test-coverage-backfill

Conversation

@piotr-roslaniec

@piotr-roslaniec piotr-roslaniec commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements implementation-plan.md Milestone 2's test-coverage backfill:
7 of the 8 listed items (item 8's scope narrowed - see below). One
earlier-planned item, a golden-value dedup test for
AssembleReservationAnchorTransaction, was obsoleted when
proposeReservationAcceptance was switched to call the exported
tbtc.AssembleReservationAnchorTransaction directly, removing the second,
unexported copy the dedup test would have compared against; it was not
silently dropped. Merged up to date with
m1/reservation-multisigner-integration-test
(#4279), tip
9e42103e8.

The 8th item (ValidateReservationAnchorProposal/
ValidateReservationReanchorProposal tests) is explicitly deferred - it
needs go-ethereum simulated-backend test infrastructure that doesn't
exist anywhere in pkg/chain/ethereum today, well beyond the plan's
0.5-day estimate. See docs/spec/reservations/m1-keep-core-readiness/01-gap-analysis.md's
new Minor row for the full finding.

Change

pkg/tbtc/reservation_test.go

  • TestAssembleReservationAnchorTransaction: happy-path output shape
    (1-in-1-out, deposit value minus fee, P2WPKH to the target wallet).
  • TestAssembleReservationReanchorTransaction: same shape assertion for
    the re-anchor sibling.

pkg/chain/ethereum/tbtc_test.go

  • TestConvertReservationParametersFromAbiType: full 10-tuple field
    mapping, every field a distinct non-zero value so a swapped or dropped
    field can't hide behind a shared zero default.
  • TestConvertReservationFromAbiType_DropsCumulativeReanchorFee: pins the
    intentional CumulativeReanchorFee omission and verifies every other
    field maps correctly around it.

pkg/tbtcpg/reservation_acceptance_test.go

  • TestReservationAcceptanceTask_BoundaryChecks: table covering
    at-limit/one-over-limit boundary crossings for MaxReservationsPerWallet,
    ReservationMinAmount, ReservationMaxTotalAmount,
    ReservationMaxSingleAmount, MaxReservationsAmountPerWallet, and
    ActiveReservationsCount, plus the net-of-fee minimum check in
    proposeReservationAcceptance -
    TestReservationAcceptanceTask_BoundedLookback only ever used these
    fields as fixture data, never at the actual boundary.
  • TestReservationAcceptanceTask_ReservationParametersFetchedLive: runs
    the same task twice against the same deposit, mutating
    ReservationMinAmount between calls - verifies a governance-driven
    parameter change takes effect on the very next Run() call, with no
    leftover value from a prior run observable in the eligibility decision.
  • TestReservationAcceptanceTask_AnchorTransactionAssembly: end-to-end
    wiring test - runs the task to get a ReservationAnchorProposal, then
    re-assembles and signs the anchor transaction via the exported
    tbtc.AssembleReservationAnchorTransaction, and asserts the resulting
    signed transaction is a valid 1-input-1-output transaction paying the
    correct wallet P2WPKH output script with value equal to deposit amount
    minus the anchor fee.

Testing

  • go test ./pkg/tbtc/... ./pkg/tbtcpg/... ./pkg/chain/ethereum/...:
    280/280 pass.
  • go build ./... && go test ./...: full repo, 49 packages, zero FAIL.
  • gofmt -l / go vet: clean on all changed/new files.

Not in this PR

  • ValidateReservationAnchorProposal/ValidateReservationReanchorProposal
    tests - deferred, documented in the gap-analysis doc.

Closes gap-analysis Major row 1 and implementation-plan.md M1 row 3.

ReservationAnchorProposal, ReservedRedemptionProposal,
ReservationReanchorProposal, and ReservationDissolutionProposal
previously used a JSON Marshal/Unmarshal placeholder, unlike every
other CoordinationProposal type in this package (Heartbeat,
DepositSweep, Redemption, MovingFunds, MovedFundsSweep), which all
marshal via pkg/tbtc/gen/pb.

Added the four missing message types to message.proto and
regenerated message.pb.go (protoc 3.21.12 installed for this).
Moved the four proposals' Marshal/Unmarshal from reservation.go's
JSON stubs into marshaling.go, matching the existing proto-based
implementations' structure and field-encoding conventions (big.Int
fees via .Bytes()/SetBytes(), fixed-size hashes/pubkey-hashes via
byte-slice copy with a length check).

Preserved the original JSON stubs' validation intent under proto3's
zero-value-is-absence semantics: a request nonce of 0, or empty
fee/reservation-key/hash bytes, are rejected the same way an
explicitly-missing JSON field was. The original '== nil' checks on
*big.Int fields don't carry over as-is - SetBytes never returns nil -
so they're now byte-length checks on the wire field instead, which is
the pattern every other proto-based proposal in this file already
uses.

Testing: extended the existing table-driven
TestCoordinationMessage_MarshalingRoundtrip with the four new types
(exact field-for-field equality through the wire, matching the
existing test's own precision, not just the fuzz-style tests already
covering every sibling type) plus four new
TestFuzzCoordinationMessage_MarshalingRoundtrip_With<X>Proposal
crash-safety tests, matching the one-per-type convention. Rewrote
the pre-existing TestReservationProposals_UnmarshalRejectsMissingIntegers
(now TestReservationProposals_UnmarshalRejectsInvalidFields) to
construct real protobuf payloads instead of JSON string literals,
porting every original missing-field case plus two new structural
cases (invalid hash/pubkey-hash length) that fall out of the new
wire format.

go test ./pkg/tbtc/...: 15/15 new/changed tests pass, full package
suite passes (146s), -race clean (156s). gofmt/vet clean on all 6
changed files.
Blocker gap found while preparing the M3 multi-signer integration
test: pkg/tbtc/coordination.go's getActionsChecklist decides which
WalletActionTypes a coordination round even considers, and it never
emitted ActionReservationAnchor or ActionReservationReanchor.

pkg/tbtcpg.ProposalGenerator.Generate only runs a task whose
ActionType() appears in the checklist it's handed (tbtcpg.go:124-135)
- it never iterates pg.tasks directly. NewReservationAcceptanceTask
and NewReservationReanchorTask are registered when
config.Reservations.Enabled=true (tbtcpg.go:88-92), but with no
checklist entry, Generate's per-window loop never selected them.
Both tasks were structurally unreachable in production regardless of
PR #4276/#4277's fixes.

Every existing unit test for these tasks (reservation_acceptance_test.go,
reservation_reanchor_test.go) calls task.Run(request) directly,
bypassing getActionsChecklist/Generate entirely - which is why this
was never caught by any prior PR's test suite.

Fix: ActionReservationAnchor and ActionReservationReanchor are now
appended unconditionally, checked on every coordination window like
ActionRedemption (both are custody-critical - an unaccepted
reservation or a stale re-anchor risks stranding, not just reduced
throughput - unlike the frequency-gated sweep/moving-funds actions).
A node with reservations disabled safely no-ops on these: Generate
already treats a checklist action with no matching registered task
as 'unsupported' and skips it without error (tbtcpg.go:131-135), the
same mechanism that already gates every other optional per-node task.

Testing:
- Updated TestCoordinationExecutor_GetActionsChecklist and its
  _PostActivation sibling: every non-nil expected checklist now
  includes both new actions right after ActionRedemption, matching
  the real append order. Extended assertChecklistOrdering's priority
  map accordingly (Redemption=0, ReservationAnchor=1,
  ReservationReanchor=2, then the existing sweep/moving-funds/
  heartbeat priorities shifted).
- Added TestCoordinationExecutor_GetActionsChecklist_ReservationActionsAlwaysPresent,
  a dedicated regression guard asserting both actions are present
  across pre/post-activation and 4th/non-4th windows, decoupled from
  the large table-driven test - would fail on its own if this wiring
  regresses.
- go test ./pkg/tbtc/... ./pkg/tbtcpg/...: 476/476 pass.
- go build ./... && go test ./...: full repo, 49 packages, zero FAIL.
- gofmt/vet clean on both changed files.
…oordination

Implementation-plan.md Milestone 3, 'multi-signer simulated
integration test' item (per user decision: build the test, leave the
testnet-drill item as an agent-not-actionable tracked item since it
needs live infra and calendar time, not code).

Scales TestCoordinationExecutor_Coordinate's existing 3-operator
harness - deterministic keypairs, real per-operator localChain fakes,
a real shared netlocal.BroadcastChannel, one goroutine per operator
running coordinationExecutor.coordinate concurrently - to
ReservationAnchorProposal and ReservationReanchorProposal. This
exercises the real leader/follower coordination round-trip
(checklist generation -> leader election -> broadcast -> follower
validation -> convergence) that no mocked pkg/tbtcpg unit test can
cover, since those call task.Run(request) directly and never go
through coordinationExecutor.coordinate. It also exercises PR
#4277's protobuf marshaling of both proposal types over a real wire
round-trip, since every follower unmarshals the leader's broadcast
coordinationMessage.

Depends on PR #4278 (this branch's parent): before that fix,
ActionReservationAnchor/ActionReservationReanchor never appeared in
getActionsChecklist's output, so every operator's checklist search in
these tests would fall through to NoopProposal and fail - confirmed
by temporarily reverting the checklist fix and re-running (both new
tests failed with the expected NoopProposal mismatch), then restoring
it.

Found and fixed one bug in this test's own harness during
verification: both new tests initially shared one netlocal broadcast
channel name. getBroadcastChannel's registry is keyed by name and
never releases old channels, so under -race (which changed
goroutine/channel-delivery timing enough to surface it in ~every
run), the reanchor test's follower sometimes received a stale
broadcast left over from the anchor test's leader. Fixed by giving
each test its own channel name; re-verified stable across 10
repeated -race runs plus the full non-race and race suites.

Testing:
- go test ./pkg/tbtc/...: 365/365 pass.
- go test -race ./pkg/tbtc/...: clean, no data races, including
  -count=10 on just the two new tests.
- go build ./... && go test ./...: full repo, 49 packages, zero FAIL.
- gofmt -l / go vet: clean.
Implementation-plan.md Milestone 2, all items except the one item
(ValidateReservationAnchorProposal/ValidateReservationReanchorProposal
tests) that needs simulated-backend test infrastructure this
repository doesn't have - see docs/spec/reservations/m1-keep-core-
readiness/01-gap-analysis.md's new Minor row for that finding, and
the item-8 dedup row for the golden-value equivalence decision.

pkg/tbtc/reservation_test.go:
- TestAssembleReservationAnchorTransaction: happy-path output shape
  (1-in-1-out, deposit value minus fee, P2WPKH to the target wallet).
  Also doubles as the golden reference for pkg/tbtcpg's dedup test.
- TestAssembleReservationReanchorTransaction: same shape assertion
  for the re-anchor sibling.

pkg/chain/ethereum/tbtc_test.go:
- TestConvertReservationParametersFromAbiType: full 10-tuple field
  mapping, every field set to a distinct non-zero value so a swapped
  or dropped field can't hide behind a shared zero default.
- TestConvertReservationFromAbiType_DropsCumulativeReanchorFee: pins
  the intentional CumulativeReanchorFee omission and verifies every
  other field maps correctly around it.

pkg/tbtcpg/reservation_acceptance_test.go:
- TestReservationAcceptanceTask_BoundaryChecks: 6-case table
  (at-limit accepts / one-over rejects) for MaxReservationsPerWallet,
  ReservationMinAmount, and ReservationMaxTotalAmount -
  BoundedLookback only ever used these as fixture data, never at the
  actual boundary.
- TestReservationAcceptanceTask_ReservationParametersFetchedLive:
  runs the same task twice against the same deposit, mutating
  ReservationMinAmount between calls - proves ReservationParameters()
  is fetched live per call, not cached on the task.

pkg/tbtcpg/reservation_anchor_dedup_test.go (new file, package
tbtcpg - internal, not tbtcpg_test - to reach the unexported
function):
- TestBuildReservationAnchorTransaction_MatchesPkgTbtcGoldenOutput:
  buildReservationAnchorTransaction (pkg/tbtcpg) and
  assembleReservationAnchorTransaction (pkg/tbtc) are independently
  maintained copies of the same logic, both unexported in different
  packages - Go's visibility rules make a single test calling both
  impossible without a production-code change. This test and
  pkg/tbtc's TestAssembleReservationAnchorTransaction instead pin the
  identical golden input/output values (deposit 100000, fee 1500,
  output 98500) in each package, catching either copy drifting from
  the other without eliminating the underlying duplication (real fix
  deferred, per decision this session).

Testing:
- go test ./pkg/tbtc/... ./pkg/tbtcpg/... ./pkg/chain/ethereum/...:
  519/519 pass.
- go build ./... && go test ./...: full repo, 49 packages, zero FAIL.
- gofmt -l / go vet: clean on all 4 changed/new files.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 6214e585-8c5d-41b8-8b6d-5db2836fb723

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The earlier regeneration used whatever protoc/protoc-gen-go happened
to be installed (apt's protoc 3.21.12, go install's protoc-gen-go
v1.30.0 at HEAD) - neither matches this file's own prior stamp
(protoc-gen-go v1.28.0 / protoc v3.19.4) or any other .pb.go in the
repo (every other generated file is v1.28.0 or v1.28.1 / protoc
3.7.1-3.21.5; this file was the only v1.30.0/3.21.12 outlier).

Regenerated with protoc 3.19.4 (official release zip, not apt) and
protoc-gen-go v1.28.0 (go install pinned to that version). Diff
against the previous commit is exactly the two version-stamp lines -
message content is otherwise byte-identical, confirming the earlier
regeneration was semantically correct and this is a pure toolchain
pin, not a functional change.
…tobuf-marshaling

Reconciles this branch's protobuf-marshaling migration for reservation
proposals with the review-fix commit that landed independently on the
base branch, which performed the same migration plus additional
correctness fixes and a scope narrowing.

- message.proto/message.pb.go: drop ReservedRedemptionProposal and
  ReservationDissolutionProposal (base commit 757c6d8: 'scaffolding
  that ships with no producer, dispatch case, or wired validator in
  this milestone' - M1 is creation/custody/re-anchor only per the
  variant-B scope decision); regenerated message.pb.go from the
  resolved .proto with the pinned protoc/protoc-gen-go toolchain.
- reservation.go/marshaling.go: adopt the base's Marshal/Unmarshal for
  the two surviving types (anchor, re-anchor), which restores the
  all-zero TargetWalletPublicKeyHash guard this branch's independent
  migration had not carried; drop this branch's now-superseded
  duplicate implementation and the two removed types' scaffolding;
  added back the doc comments the base's version had dropped, matching
  every other proposal type's convention in the file.
- reservation_test.go: adopt the base's renamed, restructured
  TestReservationProposals_UnmarshalRejectsMissingIntegers (matching
  its 2-arg marshalPb(t, msg) helper and null-payload style); restored
  two byte-length validation test cases
  ('invalid deposit funding tx hash length',
  'invalid target wallet public key hash length') the base's version
  had dropped despite the corresponding Unmarshal checks still being
  live - both now pass against the merged validation order.
- marshaling_test.go: dropped the two removed types' table entries and
  dedicated fuzz round-trip tests; fixed a duplicate proto import left
  by the auto-merge.

Full repo: zero FAIL.
…-coordination-checklist

Reconciles this branch's original coordination-checklist wiring with
the review-fix commit that independently re-fixed the same wiring on
the base branch (757c6d8: 'gate reservation checklist entries solely
on the activation block, not the local per-operator config flag').

- coordination.go: the auto-merge silently duplicated the reservation
  checklist append (this branch's original unconditional append,
  right after Redemption, plus the base's corrected
  activation-block-and-frequency-gated append later in the function) -
  ActionReservationAnchor/ActionReservationReanchor would have been
  listed twice whenever both conditions held, and listed even before
  activation the rest of the time. Removed this branch's unconditional
  append; kept only the base's gated one.
- coordination_test.go: adopted the base's
  TestCoordinationExecutor_GetActionsChecklist_Reservations (activation
  + frequency gating) and its updated priority ordering
  (Redemption < DepositSweep < MovedFundsSweep < MovingFunds <
  ReservationAnchor < ReservationReanchor < Heartbeat, reservations now
  gated like the sweep actions rather than unconditional). Rewrote this
  branch's two pre-existing checklist tests
  (TestCoordinationExecutor_GetActionsChecklist,
  _PostActivation) to match: expected values recomputed by running the
  corrected production getActionsChecklist against each test's exact
  block/window inputs, not hand-derived - 24 subtests total, all
  independently verified against the same fixed implementation the
  regression guard exists to catch drift in.

Full repo: zero FAIL.
…ion-multisigner-integration-test

Auto-merged cleanly on coordination_test.go (this branch's own
additions - the multi-signer integration tests and their fixtures -
land purely after the base's content with zero line overlap), but the
merge exposed a real breakage in this branch's own new tests: both
TestCoordinationExecutor_Coordinate_ReservationAnchor and
_ReservationReanchor used coordinationBlock = 900, which predates
ReservationsActivationBlock (24559289) fixed on the base branch
(757c6d8). With the base's activation-block gate now in effect,
ActionReservationAnchor/ActionReservationReanchor never appear in the
checklist at block 900, so both tests' mock proposal generators fell
through to NoopProposal and failed.

Bumped both tests' coordinationBlock to 24559289+3511=24562800 (a
post-activation, 4th-coordination-window block, verified against the
same production getActionsChecklist this session already confirmed
correct on the base branch). Leader election is unaffected by this
change and stays at operator2: the coordination seed depends only on
the wallet public key hash and the injected safe-block hash - both
fixed regardless of which raw block number the test passes in - not on
the coordination block number itself. Corrected
newReservationCoordinationWallet's doc comment, which had claimed
leader election was tied to block 900 specifically.

Full repo: zero FAIL.
…servation-test-coverage-backfill

Reconciles this branch's M2 test-coverage backfill work with several
independent changes on the base: dead-code removal, the conversion
function rename, and a new net-of-fee minimum-amount gate.

- Deleted pkg/tbtcpg/reservation_anchor_dedup_test.go entirely: its
  sole test pinned buildReservationAnchorTransaction (tbtcpg package)
  against a golden value shared with pkg/tbtc's sibling function, to
  catch drift between the two independently-maintained duplicate
  implementations. c48d17b deleted buildReservationAnchorTransaction
  from production code (reservation_acceptance.go now derives the
  reservation key from the anchor UTXO instead of reassembling the
  transaction) - the duplication this test existed to guard against no
  longer exists, so the guard is moot, not just broken.
- pkg/tbtc/reservation_test.go: fixed two tests
  (TestAssembleReservationAnchorTransaction,
  TestAssembleReservationReanchorTransaction) left calling the
  pre-rename lowercase assembleReservation{Anchor,Reanchor}Transaction;
  both functions were exported and gained a required
  *ReservationAction parameter (fee-ceiling guard) upstream. Updated
  call sites and doc comments; both tests' own value remains real
  (happy-path output-shape coverage the nearby InputValidation test
  doesn't provide).
- pkg/chain/ethereum/tbtc_test.go: this branch and the base
  independently added TestConvertReservationParametersFromAbiType with
  different fixture values (both equally rigorous); kept this branch's
  version (doc-linked to the gap-analysis finding it backfills) and
  the base's two genuinely new, non-overlapping tests
  (TestConvertReservationFromAbiType,
  TestConvertReservationActionFromAbiType).
- pkg/tbtcpg/reservation_acceptance_test.go: same collision pattern -
  this branch's TestReservationAcceptanceTask_ReservationParametersFetchedLive
  and the base's TestReservationAcceptanceTask_GetWalletError are
  unrelated tests that landed at the same insertion point; kept both.
  Fixed three latent fixture gaps this merge surfaced in this branch's
  own ReservationParametersFetchedLive/BoundaryChecks tests (pre-dating
  this merge, never run against current production code until now):
  missing EndBlock on the DepositRevealedEventFilter, missing Vault on
  the DepositRevealedEvent (both added by c48d17b's EndBlock/vault-
  match hardening), and a missing fee-oracle rate
  (SetEstimateSatPerVByteFee) now required by dynamic fee estimation.
  Also corrected the 'exactly at minimum' boundary case's deposit
  amount: proposeReservationAcceptance now enforces the minimum against
  the *net-of-fee* anchor value, not the gross deposit amount, so the
  boundary must be reservationMinAmount + the fixture's deterministic
  710-sat fee, not reservationMinAmount itself.

Full repo: zero FAIL.
@piotr-roslaniec
piotr-roslaniec force-pushed the m1/reservation-multisigner-integration-test branch from cd64125 to 4b7ee24 Compare September 3, 2026 11:09
Fixes 18 confirmed findings from the PR #4280 review (agent-docs/reviews/pr-4280/)
plus corrections for merge-step data defects found while auditing the raw
per-lens recall against the synthesized findings.json:

pkg/tbtcpg/reservation_acceptance_test.go:
- Fix tautological ReservationParametersFetchedLive test: ReservationParameters()
  now returns a deep copy so test-side mutation cannot corrupt the production
  candidate cache; add a no-mutation control subtest.
- Add TestReservationAcceptanceTask_AnchorTransactionAssembly to lock the
  anchor-transaction wiring (output script/value) that was previously
  entirely untested.
- Add TestReservationAcceptanceTask_GetReservationError pinning the current
  GetReservation-error fallthrough behavior.
- Extend BoundaryChecks with a gross-passes/net-fails row and at-limit/
  one-over coverage for the three previously untested eligibility gates
  (single-deposit, aggregate-wallet-amount, active-count caps).
- Extract setupEligibleDeposit helper, removing 6x fixture duplication and
  inert reservedDeposits assignments.
- Strengthen BoundedLookback to assert the specific drop path.
- Correct doc-comment line citations and restore GetWalletError's comment.
- Name the 710 sat fee constant with an accurate derivation comment.

pkg/tbtc/reservation_test.go:
- Fix TestAssembleReservationAnchorTransaction to use a distinct target
  wallet (was tautologically reusing the deposit's source wallet hash).
- Correct docstrings overstating what prior tests did NOT cover; drop the
  dangling external gap-analysis doc references.
- Note the tbtcpg-side reanchor-assembly coverage gap in the test docstring.

pkg/chain/ethereum/tbtc_test.go:
- Restore named vaultAddress variable (DRY regression).
- Pad address literals to 40 hex digits (silent zero-left-pad bug).
- Fix wrong doc-comment citation for the CumulativeReanchorFee omission
  rationale.
- Replace tautological expected-value recomputation with literal constants.
- Restore domain-meaningful fixture values in place of a meaningless
  arithmetic sequence.
- Drop dangling external gap-analysis doc reference.

go build ./..., go vet, gofmt, and go test ./... (49 packages) all clean.
…ration-test' into m1/reservation-test-coverage-backfill

# Conflicts:
#	pkg/tbtc/coordination.go
#	pkg/tbtc/coordination_test.go
#	pkg/tbtc/marshaling_test.go
#	pkg/tbtc/reservation_test.go
#	pkg/tbtcpg/reservation_acceptance_test.go
Base automatically changed from m1/reservation-multisigner-integration-test to m1/reservation-coordination-checklist September 3, 2026 12:34
Base automatically changed from m1/reservation-coordination-checklist to reservations-epic September 3, 2026 13:02
A candidate deposit that cleared the gross ReservationMinAmount but
failed the net-of-fee check (or didn't cover the anchor fee at all)
was returned as the selected candidate and then aborted proposal
generation with a hard error. Nothing marked the deposit ineligible
after the abort, so the same doomed deposit was re-selected on every
subsequent Run(), permanently blocking the wallet's reservation queue
until the deposit-reveal event aged out of the ~30-day look-back
window.

Move both fee viability checks (anchor fee coverage and net-of-fee
minimum) into findReservationAcceptanceCandidate's selection loop, so
a candidate that fails either check is skipped in favor of the next
one instead of halting the pipeline. proposeReservationAcceptance now
reuses the fee already computed during selection instead of
recomputing and re-validating it.

Also documents (does not change) the pre-existing fail-open handling
of GetReservation errors: its interface contract signals "not found"
as an error, so failing closed there would reject every brand-new
candidate; a RequestNonce==1 assertion was added to
TestReservationAcceptanceTask_GetReservationError to pin the
intentional fallback.
Fixes 20 P1/P2/P3 findings from a multi-agent review of this PR's own
test coverage:

pkg/tbtcpg/reservation_acceptance_test.go:
- Narrow the PastDepositRevealedEvents override to swallow only its
  claimed sentinel; add error-injection coverage.
- Assert AnchorTransactionAssembly's returned proposal fields against
  the task-derived candidate, not just a hand-built fixture object;
  make the fixture's ValidateReservationAnchorProposal genuinely check
  the funding outpoint instead of always returning nil.
- Delete the fixture's shadow ReservationParameters override/field;
  route through the base LocalChain's already-correct value-copy
  setter/getter instead.
- Fix a test comment overclaiming exclusion is "strictly attributable"
  to one filter when multiple fixture gaps independently cause it.
- Make the freshness-control test's RequestReservationAcceptance
  override actually record an event, so the dedup guard it's meant to
  exercise engages for real.
- Pin RequestNonce==1 for the documented GetReservation fail-open path.
- Make BoundaryChecks' three cap fields pointer-typed so a row can
  express production's real "0 = unlimited" semantic; add rows proving
  it.
- Delete the dead reservedDeposits field/IsReservedDeposit override
  (zero production readers).
- Add TestReservationAcceptanceTask_ValidateProposalError covering the
  previously-unexercised validateErr wrapping path.
- Fix two misleading comments (wrong function attribution for the
  ReservationMinAmount gate; wrong description of the GetWallet error
  contract).
- Add testReservationVaultAddress const, replacing 30+ inline literal
  duplicates.
- Extract newBoundaryTestChain helper, replacing a ~13-line setup block
  duplicated across 16 call sites.

pkg/tbtcpg/chain_test.go:
- Fix binary.BigEndian.PutUint64 writing EndBlock bytes into the
  startBlock buffer instead of endBlock across 5 call sites, so
  EndBlock actually contributes to the event-filter cache key.

pkg/chain/ethereum/tbtc_test.go:
- Replace a stale tbtc.go line-range doc citation with a symbol
  reference.
- Fold TestConvertReservationFromAbiType_DropsCumulativeReanchorFee
  into TestConvertReservationFromAbiType as a subtest, matching this
  file's one-test-per-converter convention.

pkg/chain/ethereum/tbtc.go:
- Add an in-tree TODO marking ValidateReservationAnchorProposal's
  deferred test coverage, since the docs describing that deferral
  don't exist in this checkout.
piotr-roslaniec added a commit that referenced this pull request Sep 3, 2026
…nt review of #4282) (#4283)

## Summary

Remediation for the 37 confirmed findings from a multi-agent review of
PR #4282
(`dev` <- `reservations-epic`, i.e. the accumulated content of
#4274+#4276+#4277).
37 raised -> 37 confirmed -> 0 dropped after arbitration and validation.

- **P1 (6 of 7 fully fixed, 1 partially fixed):** deposit-sweep
reservation-vault
exclusion, reservation look-back underflow + target-wallet check,
reservation
acceptance `eth_getLogs` bounds + nonce reconciliation + caps, SPV
proof-loop
retry-eviction data loss (symptom fixed, structural root cause deferred
- see
below), stale-deposit timeout memoization, below-dust re-anchor trigger
removal
  (M-27, resolved via tbtc-v2 source after user escalation).
- **P2/P3 (22 of 30 fixed, 8 explicitly deferred):** see "Deferred"
below.

Full-repo `go build`, `go vet`, and `go test ./...` all pass with these
fixes
applied (verified after every commit and once more at closeout).

## Deferred (1 P1 architectural root-cause + 7 P2/P3 symptoms/hygiene)

An arbiter-recommended structural fix for M-16 (remove the SPV proof
loop's
persistent-cursor design entirely in favor of the stateless
bounded-rescan
pattern every sibling proof type already uses) was attempted together
with
the M-7 nonce-aware timeout fix and an M-14 dead-code removal. That
combined
change broke three existing tests and was reverted rather than debugged
under
time pressure. Only a narrower, independently-safe subset landed: a
surgical
patch for M-3 (non-lossy cursor rewind) plus unrelated
memoization/metrics/test
fixes. **M-16's own P1 rating is only partially addressed** - the
persistent-cursor
design itself, and the M-7/M-14 symptoms it also breeds, remain
unremoved.

1. **M-16 (P1)** `pkg/maintainer/spv/reservation_proof_loop.go:227-246`
-
`reservationProofScanState`'s persistent cursor is the structural root
cause of M-3 (fixed surgically) and M-7 (below). Removing it in favor of
   the stateless bounded-rescan pattern is what broke 3 tests on first
   attempt and remains unimplemented.
2. **M-7 (P2)** `reservation_action_timeout_watch.go:260-281` -
`CheckReservationActionTimeouts` deletes `pendingActions` entries on 3
of
   4 non-notifying outcomes without asserting the tracked `requestNonce`
   against the freshly-derived one; same root cause as M-3.
3. **P2** `reservation_action_timeout_watch.go:370` +
`reservation_wiring.go:38-49` -
the timeout watcher's `WalletMembersResolver` only resolves wallets the
   local operator co-signs; an offline/disabled/colluding wallet's own
   operators get zero independent timeout coverage.
4. **P2** dead-code cluster in `reservation_proof_loop.go` /
`reservation_proof_loop_test.go` -
`findReservationAcceptanceTransaction`,
`findReservationReanchorTransaction`, and their wrapper helpers have
zero
production callers; 14 tests exercise the unused wrapper instead of the
   `isMatching*` predicates actually called in production.
5. **P2** `reservation_proof_loop.go:612,~817` - two tautological guards
are
algebraically always-false, masking that the real enforced constraint is
   only `0 < fee <= TxMaxFee`.
6. **P2** `reservation_wiring.go:237-320` `startStaleDepositPoll` - the
entire loop body runs untested inside a goroutine; existing tests assert
   only that the goroutine starts.
7. **P3** `reservation_action_timeout_watch.go:18-20` - unused
   "backward-compatibility alias" constant, zero references.
8. **P3** `reservation_proof_loop.go:644` - duplicated, truncated
comment
   fragment left by a merge.

## Known conflicts with other open PRs in this stack - read before
merging

This branched from `reservations-epic` at `bb3dcb398`. Three other
efforts are
in flight against overlapping code and were **not** reconciled here,
since they
belong to PRs this one doesn't own:

### 1. `pkg/tbtc/coordination.go` vs #4278 (hard conflict, not cosmetic)

#4278 ("remove frequency gate on reservation checklist actions") drops
`&& windowIndex%frequencyWindows == 0` from the reservation-actions
checklist
gate (custody-critical, should run every window like `ActionRedemption`)
but
its diff still references the old single `ReservationsActivationBlock`
constant. This PR's `602d0ef11` independently rewrote that same `if`
into
`reservationsActivationBlock(ce.ethereumNetwork)`, a per-network table
lookup
(`ethereum.Mainnet: 26500000`, everything else defaults to 0).

**A conflict resolution that naively favors this PR's side of that hunk
silently reinstates the frequency gate #4278 deliberately removed.**
Combined
resolution (verified against both intents):

```go
// Reservation actions (acceptance, re-anchor) are custody-critical like
// Redemption and are checked on every coordination window once the
// activation block is reached, not frequency-gated like the
// throughput-driven DepositSweep/MovedFundsSweep/MovingFunds actions
// above: a delayed reservation acceptance or re-anchor risks the
// on-chain ReservationActionTimeout backstop firing before the wallet
// subsystem gets a chance to act. The activation block is a per-network
// table (reservationsActivationBlock), not a single global constant, but
// it is still config-independent and globally observable from chain
// height alone -- which is what keeps leader and follower checklists in
// agreement without relying on local config.
if coordinationBlock >= reservationsActivationBlock(ce.ethereumNetwork) {
    actions = append(actions, ActionReservationAnchor)
    actions = append(actions, ActionReservationReanchor)
}
```

### 2. `pkg/tbtc/marshaling.go` vs #4278 (duplicate, this PR's version
wins)

#4278 independently adds the same 4 missing `Marshal`/`Unmarshal` doc
comments
this PR's `7cbb8cc2f` adds, but comment-only and with a capitalization
bug
(lowercases the exported type name, e.g. `"...converts the
reservationAnchorProposal..."`). This PR's version is a superset:
correctly
capitalized comments plus the actual nil-guard/zero-hash-rejection logic
#4278 doesn't have. On merge, take this PR's 4 lines, drop #4278's.

### 3. `pkg/tbtcpg/reservation_acceptance_test.go` vs #4280 (whole-file
conflict + one real design decision)

#4280 ("M2 test-coverage backfill") independently rewrote large parts of
the
same shared test harness this PR's `726f05ed7` touched - the same
`reservationAcceptanceLocalChain` type, constructor, and ~14 shared
methods,
plus `scenarioReservationAcceptanceChain`/`registerReservedDeposits`/
`expectedAnchorsEqual`. This is a heavy line-level conflict across the
whole
file, not just redundant test names. Specifics:

- `TestReservationAcceptanceTask_AmountCapBoundaries` (this PR, cap
boundaries
only) is a strict subset of #4280's
`TestReservationAcceptanceTask_BoundaryChecks`
  (adds `MaxReservationsPerWallet`, net-of-fee `ReservationMinAmount`,
`ActiveReservationsCount`). Left in place rather than deleted
preemptively -
#4280 is still open and two-deep-stacked (on #4278, also open) and could
  stall or be reworked; delete this PR's version only in the merge that
  actually lands #4280.
- This PR's
`TestReservationAcceptanceTask_VaultNotConfigured_ZeroAddress`
(finding: dead vault-not-configured guard) has no equivalent on #4280's
  side - a "just take #4280's file" resolution silently drops it.
- **Real design decision, not just a merge conflict:** #4280's
  `TestReservationAcceptanceTask_Stateless_PastEventsError` exercises
`PastReservationAcceptanceRequestedEvents` returning an error and
asserts
fail-closed skip-on-error. This PR's `hasPendingAction` (from
`726f05ed7`)
no longer calls `PastReservationAcceptanceRequestedEvents` at all - it
uses
a different, generation-scoped pending-action check instead. Ported onto
this PR's code as-is, that test would either pass vacuously or fail for
an
unrelated reason. **This PR intentionally left
`PastReservationAcceptanceRequestedEvents`
  on the `tbtcpg.Chain` interface (`chain.go:263`) and the test double's
`acceptanceEvents`/`acceptanceEventsErr` fields in place, undeleted,
even
though they now have zero production callers** - removing them here
would
have foreclosed reconciling #4280's test against whichever
pending-action
mechanism is ultimately kept. Whoever merges this PR and #4280 needs to
pick one mechanism and either delete the losing side's interface
method/test
  or keep both if there's a reason for two independent checks.

## Testing

- `go build ./...`, `go vet ./...`: clean.
- `go test ./...`: full repo suite, 0 failures (verified at closeout
after
  every commit landed).
@piotr-roslaniec
piotr-roslaniec marked this pull request as ready for review September 3, 2026 15:42
…vation-test-coverage-backfill

# Conflicts:
#	pkg/tbtcpg/reservation_acceptance.go
#	pkg/tbtcpg/reservation_acceptance_test.go
The 'records acceptance request, skipping duplicate on subsequent run'
test relied on hasPendingAction's fail-closed default on a missing
GetReservationAction record, not on the intended pending-state
detection path - its comment still described the removed
PastReservationAcceptanceRequestedEvents mechanism. Explicitly set
the action record to Pending after the first run so the second run's
dedup assertion genuinely exercises hasPendingAction's happy path.
@piotr-roslaniec
piotr-roslaniec merged commit 356d35b into reservations-epic Sep 3, 2026
17 checks passed
@piotr-roslaniec
piotr-roslaniec deleted the m1/reservation-test-coverage-backfill branch September 3, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant